/**
** PC Speaker Routines By Neil C. Obremski                    [ Magenta's Maze ]
** 2017 Gibdon Moon Productions                   [ http://magsmaze.gibdon.com ]
**
** This is free and unencumbered software released into the public domain.
**
** Anyone is free to copy, modify, publish, use, compile, sell, or
** distribute this software, either in source code form or as a compiled
** binary, for any purpose, commercial or non-commercial, and by any
** means.
**
** In jurisdictions that recognize copyright laws, the author or authors
** of this software dedicate any and all copyright interest in the
** software to the public domain. We make this dedication for the benefit
** of the public at large and to the detriment of our heirs and
** successors. We intend this dedication to be an overt act of
** relinquishment in perpetuity of all present and future rights to this
** software under copyright law.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
** OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
** OTHER DEALINGS IN THE SOFTWARE.
**
** Tested w/ DOSBox 0.74, Microsoft C 5.10
 */
#ifndef __MAG_SPKR
#define __MAG_SPKR

#include <stdio.h>
#include <time.h>

#include "mag-bios.h"

#ifndef AMPLIFY
#define AMPLIFY 9
#endif

#ifndef MUTING
#define MUTING 2
#endif

/* turn ON PC Speaker (set bits 1&2 of port 61h) */
#define PC_SPEAKER_ON() outp(0x61, inp(0x61) | 3)

/* turn OFF PC Speaker (clear bits 1&2 of port 61h) */
#define PC_SPEAKER_OFF() outp(0x61, inp(0x61) & 0xFC)

#define SFX_BUMP() sound(74,2)
#define SFX_PICKUP() sound(200,2)
#define SFX_PUTDOWN() sound(700,2)
#define SFX_BEEP() sound(1000,1)

/* old INT 8H timer interrupt; saved by voice() prior to bangpit2() */
static void (interrupt far *INT8H)(void);

/* used by initemb() when loading embedded samples */
#ifndef MAX_EMBED_SIZE
#define MAX_EMBED_SIZE 12000
#endif

/* global used by initemb() */
unsigned char far *embwav[] = {
	(unsigned char far*)0, (unsigned char far*)0, (unsigned char far*)0,
	(unsigned char far*)0, (unsigned char far*)0, (unsigned char far*)0,
	(unsigned char far*)0, (unsigned char far*)0, (unsigned char far*)0,
	(unsigned char far*)0, (unsigned char far*)0, (unsigned char far*)0,
	(unsigned char far*)0, (unsigned char far*)0, (unsigned char far*)0,
	(unsigned char far*)0, (unsigned char far*)0, (unsigned char far*)0,
	(unsigned char far*)0, (unsigned char far*)0 };

/* globals used by bangpit2() */
volatile unsigned int bangreset;
volatile unsigned char far *bangbits;

int bangbyte(void);
void interrupt far bangpit2(void);
void initemb(char *);
unsigned char far *loadsample(char*);
#define normalize(c) (((c)>>MUTING)+AMPLIFY) /* mute&amplify sound byte */
#define playemb(i) voice(embwav[i], 8000) /* plays embedded sample */
void sound(int, int);
void quitemb(void);
void voice(unsigned char far *, unsigned int);

/* current byte to be pushed to PIT#2 */
int bangbyte(void)
{
	/* this function prevents compiler optimizations (MSC 5.1 /Ox) from
	optimizing out the update of the global pointer */
	return (int)(*bangbits);
}

/* bangs PIT channel 2 (PC Speaker) with bytes in bangbits */
void interrupt far bangpit2(void)
{
	static int oversample = 2;
	static unsigned int counter = 0;
	int soundbyte = (int)(*bangbits);

	outp(0x42, soundbyte);	/* set PIT#2 counter (make sure it's one-shot!) */

	if (!--oversample) {
		if (soundbyte) {	/* stop on first 0 byte */
			oversample = 2;	/* play each byte twice for 2x oversampling */
			bangbits++;
		}
	}

	counter -= bangreset;
	if (counter > bangreset) {
		outp(0x20, 0x20);	/* signal End Of Interrupt (EOI) to PIC directly */
	} else {
		INT8H();			/* call old timer interrupt (signals PIC, etc.) */
	}
}

/* load embedded WAV files into global embwav[] array */
void initemb(char *fn)
{
	unsigned long RIFF = *((unsigned long *)"RIFF");
	unsigned long WAVE = *((unsigned long *)"WAVE");
	unsigned char buf[128]; /* MSC 5.1 has a really small stack size */
	unsigned char far *wp = (unsigned char far*)0;
	int amount, i, effect = 0;
	FILE *fp = fopen(fn, "rb+");

	if (!fp) {
		return;
	}

	/* TODO: BUGBUG: this loads the INFO block in the WAV data as sample data */

	/* TODO: also, this is REAAAAAAALLLLLY slow on the Tandy 1000 from floppy */

	while (sizeof(buf) == (amount = fread(buf, 1, sizeof(buf), fp))) {
		for (i = 0; i < sizeof(buf) - 12; i++) {
			if (RIFF == *((unsigned long*)&buf[i+0]) &&
				WAVE == *((unsigned long*)&buf[i+8])) {
				wp = embwav[effect++] = _fmalloc(MAX_EMBED_SIZE);
				fseek(fp, (long)(i - sizeof(buf)) + 44L + 12L, SEEK_CUR);
				printf("Loading embedded WAV #%d @ %ld\n",
					(effect-1), ftell(fp));
				for (i = 0; i < MAX_EMBED_SIZE; i++) { /* RE-USING 'i' */
					wp[i] = (unsigned char)0;
				}
				break;
			} else if (wp) {
				*wp++ = normalize(buf[i]);
			}
		}
		fseek(fp, -12L, SEEK_CUR);
	}
	if (wp) { /* read final bits */
		for (i = 0; i < amount; i++) {
			*wp++ = normalize(buf[i]);
		}
	}

	fclose(fp);
}

/* loads a digitized sample from a VOC or WAV file, returns allocated far ptr */
unsigned char far *loadsample(char *fn)
{
	FILE *fp = fopen(fn, "rb+");
	int size;
	unsigned char far *sample = (unsigned char far*)0;
	unsigned char far *wp;
	unsigned char buf[128];
	size_t amount;
	int i;

	if (!fp) {
		return sample;
	}

	fseek(fp, 0L, SEEK_END);
	size = (int)ftell(fp);
	fseek(fp, 0x2CL, SEEK_SET);

	wp = sample = _fmalloc(size - 0x2C + 1);
	sample[size - 0x2C] = (unsigned char)0;

	while (sizeof(buf) == (amount = fread(buf, 1, sizeof(buf), fp))) {
		for (i = 0; i < sizeof(buf); i++) {
			*wp++ = normalize(buf[i]);
		}
	}

	for (i = 0; i < amount; i++) {
		*wp++ = normalize(buf[i]);
	}

	fclose(fp);

	return sample;
}

/* plays a sound at a certain frequency for the given number of ticks */
void sound(int frequency, int ticks)
{
	int d = (int)(1193180L / frequency);

	if (frequency < 37 || frequency > 20000 || ticks < 1 || ticks > 36) {
		return;
	}

	outp(0x43, 0xB6);	/* PIT #2: 16-bit access, Mode 3 (square wave) */

	outp(0x42, (int)(d & 0xFF) );			/* send counter lo-byte */
	outp(0x42, (int)((d >> 8) & 0xFF) );	/* send counter hi-byte */

	PC_SPEAKER_ON();

	wait(ticks);	/* ticks is INTEGER whereas [Q]BASIC accepts a float */

	PC_SPEAKER_OFF();
}

/* releases memory used by voice sound effects */
void quitemb(void)
{
	int i;
	for (i = 0; i < 10; i++) {
		if (embwav[i]) {
			_ffree(embwav[i]);
		}
		embwav[i] = (unsigned char far *)0;
	}
}

/* plays a digitized 8khz sample by "bit banging" PIT#2 */
void voice(unsigned char far *sample, unsigned int hz)
{
	unsigned char far *beginning = sample;
	unsigned counter, start, t;
	unsigned long safety = ticker() + 144; /* ~8 seconds (64k @ 8khz) */
	int soundbyte;

	if (!sample || hz < 4000) {
		return;
	}

	counter = (unsigned int)(1193180L / hz);

	start = pitwait(counter, &t);	/* test computer speed */

	outp(0x43, 0x90);		/* PIT #2: 8-bit access, Mode 0 (one shot) */

	if (start <= 1) {		/* use fixed delay code for very slow computers */

		if (!start && hz > 8000) {
			return;			/* don't attempt high frequency on slow computer */
		}

		outp(0x43, 0);
		_disable();			/* CLI (turn off interrupts for duration) */
		PC_SPEAKER_ON();
		for (t = 0; soundbyte = (int)sample[t>>1]; t++) {	/* 2x oversample */
			outp(0x42, soundbyte);
			start = (unsigned)inp(0x40);		/* this causes a decent delay */
			start |= ((unsigned)inp(0x40)) << 8;/* for 8khz on Tandy 1000 */
		}

		PC_SPEAKER_OFF();
		_enable();			/* STI (turn interrupts back on) */
		return;
	}

#ifdef NDEBUG
	bangbits = sample;		/* sound byte ptr used by bangpit2() & bangbyte() */

	counter >>= 1;			/* 2x oversample so we must run 2x fast */
	bangreset = counter;	/* used to determine when to call original INT 8h */

	PC_SPEAKER_ON();

	INT8H = _dos_getvect(0x08);		/* save old INT8H (BIOS tick count) */
	_dos_setvect(0x08, bangpit2);	/* switch to our INT8H for PIT#0's IRQ0 */

	outp(0x43, 0x36);				/* reset PIT#0 counter (Mode 3) */
	outp(0x40,(int)(counter & 0xFF)); outp(0x40,(int)((counter>>8)&0xFF));

	if (start < (counter >> 3)) {	/* slow computer (no idle CPU)*/
		while (bangbyte()) {
			if (ticker() > safety) {
				break;				/* sound exceeded max time (~8 seconds) */
			}
		}
	} else {						/* fast computer (need idle time) */
		do {
			if (ticker() > safety) {
				break;				/* sound exceeded max time (~8 seconds) */
			}

			breath();				/* give up CPU (temporarily) to system */

		} while (bangbyte());
	}

	outp(0x43, 0x36);				/* reset PIT#0 counter (Mode 3) default */
	outp(0x40, 0); outp(0x40, 0);

	_dos_setvect(0x08, INT8H);		/* restore old INT8H (BIOS tick count) */

	PC_SPEAKER_OFF();

#else
	/* in DEBUG builds we don't alter PIT#0 but instead bang in a tight loop */

	if (start < (counter >> 3)) {	/* reduce delay for slower computers */
		counter -= (((t - counter)>>2)<<2);
		if (counter > 0xFF) {
			counter = 0;
		}
	}

	_disable();
	PC_SPEAKER_ON();

	outp(0x43, 0); /* PIT #0: latch count */
	t = (unsigned)inp(0x40);
	t |= ((unsigned)inp(0x40)) << 8;

	while ( (soundbyte = (int)(*sample++)) ) {
		start = t;
		outp(0x42, soundbyte);
		do {
			t = (unsigned)inp(0x40);
			t |= ((unsigned)inp(0x40)) << 8;
		} while (start - t < counter);

		start = t;
		outp(0x42, soundbyte);
		do {
			t = (unsigned)inp(0x40);
			t |= ((unsigned)inp(0x40)) << 8;
		} while (start - t < counter);
	}

	PC_SPEAKER_OFF();
	_enable();

#endif
}

#endif
